<!DOCTYPE html>
<html lang="en">

<!-- Head tag -->
<head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <meta name="description" content="李修倩在 Github 上的个人博客">
    <meta name="keyword" content="null">
    <meta name="theme-color" content="#600090">
    <meta name="msapplication-navbutton-color" content="#600090">
    <meta name="apple-mobile-web-app-capable" content="yes">
    <meta name="apple-mobile-web-app-status-bar-style" content="#600090">
    <link rel="shortcut icon" href="https://cdn4.iconfinder.com/data/icons/ionicons/512/icon-person-128.png">
    <link rel="alternate" type="application/atom+xml" title="Xiuqianli&#39;s blog" href="/atom.xml">
    <link rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs/animate.css/3.5.2/animate.min.css">
    <link rel="stylesheet" href="//cdn.bootcss.com/font-awesome/4.6.3/css/font-awesome.css">
    <title>
        
        手写山寨spring基础功能 - JDBC｜xiuqianli&#39;s blog
        
    </title>

    <link rel="canonical" href="https://981764793.gitee.io/2018/07/28/手写山寨spring基础功能-JDBC/">

    <!-- Bootstrap Core CSS -->
    <link rel="stylesheet" href="/css/bootstrap.min.css">

    <!-- Custom CSS -->
    <link rel="stylesheet" href="/css/blog-style.css">

    <!-- Pygments Github CSS -->
    <link rel="stylesheet" href="/css/syntax.css">
</head>

<style>

    header.intro-header {
        background-image: url('http://oco8bvfr8.qnssl.com/blog-bg-black-run.jpg?imageView2/1/w/1400/h/400/interlace/1/q/80')
    }
</style>
<!-- hack iOS CSS :active style -->
<body ontouchstart="" class="animated fadeIn">
<!-- Navigation -->
<nav class="navbar navbar-default navbar-custom navbar-fixed-top " id="nav-top" data-ispost = "true" data-istags="false
" data-ishome = "false" >
    <div class="container-fluid">
        <!-- Brand and toggle get grouped for better mobile display -->
        <div class="navbar-header page-scroll">
            <button type="button" class="navbar-toggle">
                <span class="sr-only">Toggle navigation</span>
                <span class="icon-bar"></span>
                <span class="icon-bar"></span>
                <span class="icon-bar"></span>
            </button>
            <a class="navbar-brand animated pulse" href="/">
                <span class="brand-logo">
                    Xiuqianli&#39;s blog
                </span>
                's Blog
            </a>
        </div>

        <!-- Collect the nav links, forms, and other content for toggling -->
        <!-- Known Issue, found by Hux:
            <nav>'s height woule be hold on by its content.
            so, when navbar scale out, the <nav> will cover tags.
            also mask any touch event of tags, unfortunately.
        -->
        <!-- /.navbar-collapse -->
        <div id="huxblog_navbar">
            <div class="navbar-collapse">
                <ul class="nav navbar-nav navbar-right">
                    <li>
                        <a href="/">Home</a>
                    </li>
					
                    
                        
							
                        <li>
                            <a href="/Tags/">Tags</a>
                        </li>
							
						
                    
					
					
                </ul>
            </div>
        </div>
    </div>
    <!-- /.container -->
</nav>
<script>
    // Drop Bootstarp low-performance Navbar
    // Use customize navbar with high-quality material design animation
    // in high-perf jank-free CSS3 implementation
//    var $body   = document.body;
    var $toggle = document.querySelector('.navbar-toggle');
    var $navbar = document.querySelector('#huxblog_navbar');
    var $collapse = document.querySelector('.navbar-collapse');

    $toggle.addEventListener('click', handleMagic)
    function handleMagic(e){
        if ($navbar.className.indexOf('in') > 0) {
        // CLOSE
            $navbar.className = " ";
            // wait until animation end.
            setTimeout(function(){
                // prevent frequently toggle
                if($navbar.className.indexOf('in') < 0) {
                    $collapse.style.height = "0px"
                }
            },400)
        }else{
        // OPEN
            $collapse.style.height = "auto"
            $navbar.className += " in";
        }
    }
</script>

<!-- Main Content -->

<!--only post-->


<img class="wechat-title-img"
     src="http://oco8bvfr8.qnssl.com/blog-bg-black-run.jpg?imageView2/1/w/1400/h/400/interlace/1/q/80">


<style>
    
    header.intro-header {
        background-image: url('http://oco8bvfr8.qnssl.com/blog-bg-black-run.jpg?imageView2/1/w/1400/h/400/interlace/1/q/80')
    }

    
</style>

<header class="intro-header">
    <div class="container">
        <div class="row">
            <div class="col-lg-8 col-lg-offset-2 col-md-10 col-md-offset-1 text-center">
                <div class="post-heading">
                    <h1>手写山寨spring基础功能 - JDBC</h1>
                    
                    <span class="meta">
                         作者 Xiuqian Li
                        <span>
                          日期 2018-07-28
                         </span>
                    </span>
                    <div class="tags text-center">
                        
                        <a class="tag" href="/tags/#java"
                           title="java">java</a>
                        
                        <a class="tag" href="/tags/#spring"
                           title="spring">spring</a>
                        
                    </div>
                </div>
            </div>
        </div>
    </div>
    <div class="post-title-haojen">
        <span>
            手写山寨spring基础功能 - JDBC
        </span>
    </div>
</header>

<!-- Post Content -->
<article>
    <div class="container">
        <div class="row">
            <!-- Post Container -->
            <div class="col-lg-8 col-lg-offset-1 col-sm-9 post-container">
                <h4 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h4><p>之前模仿了spring的部分功能，现在来模仿一下ORM功能，可以当作是简单仿Mybatis。目前写这篇笔记的时候是有重构的思路，但是准备秋招，在复习基础知识，暂时没时间改这部分代码，所以这篇笔记先记录之前实现的部分。在写笔记的时候发现了上次尝试优化留下的bug，简单的改了改……</p>
<h4 id="实现功能"><a href="#实现功能" class="headerlink" title="实现功能"></a>实现功能</h4><p>通过接口获取Mapper，注解定义SQL语句，自动映射字段，事务。</p>
<h4 id="注解类"><a href="#注解类" class="headerlink" title="注解类"></a>注解类</h4><p>主要用到以下几个注解</p>
<figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">/**</span><br><span class="line"> * 定义sql语句的参数名</span><br><span class="line"> */</span><br><span class="line">@Target(&#123;ElementType.PARAMETER&#125;)</span><br><span class="line">@Retention(RetentionPolicy.RUNTIME)</span><br><span class="line">@Documented</span><br><span class="line">public @interface LSDbParam &#123;</span><br><span class="line"></span><br><span class="line">    String value();</span><br><span class="line"></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"></span><br><span class="line">/**</span><br><span class="line"> * 标注类是Mapper类</span><br><span class="line"> */</span><br><span class="line">@Target(&#123;ElementType.TYPE&#125;)</span><br><span class="line">@Retention(RetentionPolicy.RUNTIME)</span><br><span class="line">@Documented</span><br><span class="line">public @interface LSMapper &#123;</span><br><span class="line"></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">/**</span><br><span class="line"> * 标注方法是删除、更新或者新增</span><br><span class="line"> */</span><br><span class="line">@Target(&#123;ElementType.METHOD&#125;)</span><br><span class="line">@Retention(RetentionPolicy.RUNTIME)</span><br><span class="line">@Documented</span><br><span class="line">public @interface LSModifying &#123;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">/**</span><br><span class="line"> * 定义SQL语句</span><br><span class="line"> */</span><br><span class="line">@Target(&#123;ElementType.METHOD&#125;)</span><br><span class="line">@Retention(RetentionPolicy.RUNTIME)</span><br><span class="line">@Documented</span><br><span class="line">public @interface LSQuery &#123;</span><br><span class="line"></span><br><span class="line">    String value();</span><br><span class="line"></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"></span><br><span class="line">/**</span><br><span class="line"> * 事务注解</span><br><span class="line"> */</span><br><span class="line">@Target(&#123;ElementType.METHOD&#125;)</span><br><span class="line">@Retention(RetentionPolicy.RUNTIME)</span><br><span class="line">@Documented</span><br><span class="line">public @interface LSTransacted &#123;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>
<p>因为这个模块是想做成可单独使用的（虽然写着代码还是依赖了少部分core模块的代码，复制一下就可以解除依赖），所以LSTransacted不是必须的，只是实现配合core模块使用时的注解事务，LSMapper也是提供给IOC扫描用的。</p>
<h4 id="执行的方法和SQL直接的映射关系"><a href="#执行的方法和SQL直接的映射关系" class="headerlink" title="执行的方法和SQL直接的映射关系"></a>执行的方法和SQL直接的映射关系</h4><p>首先来一个MapperData类，保存执行的方法和SQL直接的映射关系，我这次只实现了注解SQL,其实可以在启动时从xml、json等各种文件里加载SQL语句，类似于Mybatis，只是懒得写这部分了，毕竟只是用来学习，简单模仿一下原理，也没人会用。</p>
<figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">public class MapperData &#123;</span><br><span class="line"></span><br><span class="line">    private static final String paramRegex = &quot;\\$\\&#123;(\\w+)\\&#125;&quot;;</span><br><span class="line">    private static Pattern paramNamePattern = Pattern.compile(paramRegex);</span><br><span class="line">    private String sql;</span><br><span class="line">    private List&lt;String&gt; sqlParamNames = new ArrayList&lt;&gt;();</span><br><span class="line">    private Map&lt;String, Integer&gt; sqlParamIndexMap = new HashMap&lt;&gt;();</span><br><span class="line">    private boolean modifying;</span><br><span class="line">    </span><br><span class="line">    public MapperData(Method method, String sql, boolean modifying) &#123;</span><br><span class="line">        this.sql = sql;</span><br><span class="line">        this.modifying = modifying;</span><br><span class="line">        int pos = 0;</span><br><span class="line">        //通过正则拿到参数对应的位置</span><br><span class="line">        Matcher matcher = paramNamePattern.matcher(sql);</span><br><span class="line">        while (matcher.find()) &#123;</span><br><span class="line">            String name = matcher.group(1);</span><br><span class="line">            sqlParamIndexMap.put(name, pos++);</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        //获取方法参数的注解名</span><br><span class="line">        for (Parameter parameter : method.getParameters()) &#123;</span><br><span class="line">            LSDbParam lsDbParam = parameter.getAnnotation(LSDbParam.class);</span><br><span class="line">            if (lsDbParam == null) &#123;</span><br><span class="line">                continue;</span><br><span class="line">            &#125;</span><br><span class="line">            String name = lsDbParam.value();</span><br><span class="line">            sqlParamNames.add(name);</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        //判断一下SQL语句需要的参数和方法实际能提供的参数个数是否相符</span><br><span class="line">        if (sqlParamIndexMap.size() != sqlParamNames.size()) &#123;</span><br><span class="line">            throw new LSJdbcException(&quot;The param length is invalid&quot;);</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    public String buildSql(Object[] params) &#123;</span><br><span class="line">        String realSql = sql;</span><br><span class="line">        for (Object obj : params) &#123;</span><br><span class="line">            String placeholder = &quot;?&quot;;</span><br><span class="line">            if (obj.getClass().isArray()) &#123;</span><br><span class="line">                placeholder = getPlaceholders(Array.getLength(obj));</span><br><span class="line">            &#125;</span><br><span class="line">            realSql = realSql.replaceFirst(paramRegex, placeholder);</span><br><span class="line">        &#125;</span><br><span class="line">        return realSql;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    public Object[] sortParams(Object[] params) &#123;</span><br><span class="line">        if (params.length != sqlParamNames.size()) &#123;</span><br><span class="line">            throw new LSJdbcException(&quot;The param length is invalid&quot;);</span><br><span class="line">        &#125;</span><br><span class="line">        Object[] sortedParams = new Object[params.length];</span><br><span class="line"></span><br><span class="line">        for (int i = 0; i &lt; params.length; i++) &#123;</span><br><span class="line">            int index = sqlParamIndexMap.get(sqlParamNames.get(i));</span><br><span class="line">            sortedParams[i] = params[index];</span><br><span class="line">        &#125;</span><br><span class="line">        return sortedParams;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    private String getPlaceholders(int count) &#123;</span><br><span class="line">        StringBuilder stringBuilder = new StringBuilder();</span><br><span class="line">        for (int i = 0; i &lt; count; i++) &#123;</span><br><span class="line">            if (i &gt; 0) &#123;</span><br><span class="line">                stringBuilder.append(&quot;,&quot;);</span><br><span class="line">            &#125;</span><br><span class="line">            stringBuilder.append(&quot;?&quot;);</span><br><span class="line">        &#125;</span><br><span class="line">        return stringBuilder.toString();</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    public boolean isModifying() &#123;</span><br><span class="line">        return modifying;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>
<p>其中有个buildSql方法，用于生成真正执行的带参数占位符的SQL语句，getPlaceholders用于生成数组参数的占位符，一开始是可以支持Collection类型参数的，后来懒了给干掉了，可以在buildSql还有后面的executor加一个if判断就完事。</p>
<p>写笔记的时候想起还是把这两方法还有sortParams放工具类里更好更符合类单一原则，只是当时可能有点急于求成没想这么多了。</p>
<p>还有buildSql里反复调用String的replaceFirst通过正则替换占位符其实是挺影响性能的，下次重构会改掉。</p>
<h4 id="代理类"><a href="#代理类" class="headerlink" title="代理类"></a>代理类</h4><p>通过接口类获得Mapper是通过动态代理实现的，所以需要一个继承MethodInterceptor的代理类来通过JDBC执行具体的SQL语句。</p>
<figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">public class MapperProxy implements MethodInterceptor &#123;</span><br><span class="line"></span><br><span class="line">    private SqlSession sqlSession;</span><br><span class="line"></span><br><span class="line">    public MapperProxy(SqlSession sqlSession) &#123;</span><br><span class="line">        this.sqlSession = sqlSession;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    @Override</span><br><span class="line">    public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable &#123;</span><br><span class="line">        MapperData mapperData = getMapperData(method);</span><br><span class="line">        Object[] args = mapperData.sortParams(objects);</span><br><span class="line">        String sql = mapperData.buildSql(args);</span><br><span class="line">        if (mapperData.isModifying()) &#123;</span><br><span class="line">            return sqlSession.executeUpdate(sql, args);</span><br><span class="line">        &#125;</span><br><span class="line">        Class&lt;?&gt; returnType = method.getReturnType();</span><br><span class="line">        if (List.class.isAssignableFrom(returnType)) &#123;</span><br><span class="line">            //获取返回值的类型</span><br><span class="line">            Type type = method.getGenericReturnType();</span><br><span class="line">            Class&lt;?&gt; realType;</span><br><span class="line">            if (type instanceof ParameterizedType) &#123;</span><br><span class="line">                realType = (Class) ((ParameterizedType) type).getActualTypeArguments()[0];</span><br><span class="line">            &#125; else &#123;</span><br><span class="line">                realType = (Class)type;</span><br><span class="line">            &#125;</span><br><span class="line">            return sqlSession.selectList(realType, sql, args);</span><br><span class="line">        &#125;</span><br><span class="line">        if (returnType.isArray()) &#123;</span><br><span class="line">//            System.out.println(returnType.getComponentType());</span><br><span class="line">            Class&lt;?&gt; realType = returnType.getComponentType();</span><br><span class="line">            List list = sqlSession.selectList(realType, sql, args);</span><br><span class="line">            //通过Array.newInstance创建数组保证是对应的类型</span><br><span class="line">            Object[] result = (Object[]) Array.newInstance(realType, list.size());</span><br><span class="line">            return list.toArray(result);</span><br><span class="line">        &#125;</span><br><span class="line">        if (returnType == long.class || returnType == Long.class) &#123;</span><br><span class="line">            return sqlSession.queryNumber(sql, args).longValue();</span><br><span class="line">        &#125;</span><br><span class="line">        return sqlSession.selectOne(returnType, sql, args);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    private MapperData getMapperData(Method method) &#123;</span><br><span class="line">        String fullMethodName = ClassUtil.getFullMethodName(method);</span><br><span class="line">        MapperData mapperData = MapperContainer.mapperMethodMap.get(fullMethodName);</span><br><span class="line">        if (mapperData == null) &#123;</span><br><span class="line">            if (!method.isAnnotationPresent(LSQuery.class)) &#123;</span><br><span class="line">                //如果没有找到对应的SQL映射并且这个方法又没有LSQuery注解修饰就抛出异常</span><br><span class="line">                throw new LSJdbcException(String.format(&quot;can not find mapper: %s&quot;, fullMethodName));</span><br><span class="line">            &#125;</span><br><span class="line">            LSQuery lsQuery = method.getAnnotation(LSQuery.class);</span><br><span class="line">            String sql = lsQuery.value();</span><br><span class="line">            boolean modifying = method.isAnnotationPresent(LSModifying.class);</span><br><span class="line">            mapperData = new MapperData(method, sql, modifying);</span><br><span class="line">            MapperContainer.mapperMethodMap.put(fullMethodName, mapperData);</span><br><span class="line">        &#125;</span><br><span class="line">        return mapperData;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>
<p>整个代码的流程就是：根据方法拿到对应的MapperData -&gt; 对参数进行排序 -&gt; 构建真正的SQL语句 -&gt; 如果是更新就调用更新方法，否则根据方法返回值类型进行判断调用相应的查询方法。</p>
<p>比较重点的有获取List的泛型类型和通过Array类创建数组。</p>
<p>其实最后那个return sqlSession.selectOne(returnType, sql, args);还是不严谨，因为可能是其他基本类型，考虑加判断。</p>
<p>getMapperData里用到MapperContainer，其实里面就是个HashMap而已，暂时把这个Map先单独放出去，防止后续有其他的想法。</p>
<p>其中ClassUtil.getFullMethodName可能会影响部分性能，考虑加个Map缓存。</p>
<h4 id="实现SqlSession"><a href="#实现SqlSession" class="headerlink" title="实现SqlSession"></a>实现SqlSession</h4><p>SqlSession是一个接口，暂时只有一个MySqlSession实现类，其实其他数据库用也没毛病，反正都是调的JDBC，只是考虑到后面可能会加一个直接查询分页的功能暂且这样设计。</p>
<figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">public class MySqlSession implements SqlSession &#123;</span><br><span class="line"></span><br><span class="line">    private LSDbConfiguration configuration;</span><br><span class="line">    private JdbcExecutor executor;</span><br><span class="line"></span><br><span class="line">    public MySqlSession(LSDbConfiguration configuration) &#123;</span><br><span class="line">        this.configuration = configuration;</span><br><span class="line">        this.executor = configuration.getExecutor();</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    @Override</span><br><span class="line">    public &lt;E&gt; E getMapper(Class&lt;?&gt; clazz) &#123;</span><br><span class="line">        return (E) Enhancer.create(clazz, new MapperProxy(this));</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    @Override</span><br><span class="line">    public &lt;T&gt; T selectOne(Class&lt;T&gt; clazz, String sql, Object[] params) &#123;</span><br><span class="line">        return executor.queryBean(getConnection(), clazz, sql, params);</span><br><span class="line"></span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    @Override</span><br><span class="line">    public &lt;T&gt; List&lt;T&gt; selectList(Class&lt;T&gt; clazz, String sql, Object[] params) &#123;</span><br><span class="line">        return executor.queryBeanList(getConnection(), clazz, sql, params);</span><br><span class="line"></span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    @Override</span><br><span class="line">    public long executeUpdate(String sql, Object[] params) &#123;</span><br><span class="line">        return executor.executeUpdate(getConnection(), sql, params);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    @Override</span><br><span class="line">    public Number queryNumber(String sql, Object[] params) &#123;</span><br><span class="line">        return executor.queryNumber(getConnection(), sql, params);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    private Connection getConnection() &#123;</span><br><span class="line">        Connection connection = ConnectionContext.get();</span><br><span class="line">        if (connection == null) &#123;</span><br><span class="line">            DataSource dataSource = configuration.getDataSource();</span><br><span class="line">            if (dataSource == null) &#123;</span><br><span class="line">                throw new LSJdbcException(&quot;DataSource is null&quot;);</span><br><span class="line">            &#125;</span><br><span class="line">            try &#123;</span><br><span class="line">                connection = dataSource.getConnection();</span><br><span class="line">                ConnectionContext.set(connection);</span><br><span class="line">            &#125; catch (SQLException e) &#123;</span><br><span class="line">                e.printStackTrace();</span><br><span class="line">                throw new LSJdbcException(e);</span><br><span class="line">            &#125;</span><br><span class="line">        &#125;</span><br><span class="line">        return connection;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>
<p>其中用到的LSDbConfiguration类其实目前只是保存着数据源Datasource和JdbcExecutor，这个类意义不大。主要还是JdbcExecutor。</p>
<p>还用到了一个ConnectionContext类，里面就是给ThreadLocal，保存着当前线程的数据库连接，用于后面实现数据库事务。每次执行SQL语句需要数据库连接的时候调用getConnection方法，先尝试从ConnectionContext里拿，拿不到就从找dataSource要，然后放进ConnectionContext里。</p>
<p>其余几个方法就是供代理类调用的方法，都是直接调用了executor的方法，说白了SqlSession就是做了一层包装而已，大概可以相当于装饰者模式。</p>
<h4 id="ConnectionContext"><a href="#ConnectionContext" class="headerlink" title="ConnectionContext"></a>ConnectionContext</h4><p>这里面维护着数据库连接，还有着开启事务、提交事务、回滚、关闭连接的封装方法。</p>
<figure class="highlight plain"><table><tr><td class="code"><pre><span class="line"></span><br><span class="line">public class ConnectionContext &#123;</span><br><span class="line"></span><br><span class="line">    private static ThreadLocal&lt;Connection&gt; connectionHolder = new ThreadLocal&lt;&gt;();</span><br><span class="line">    private static ThreadLocal&lt;Boolean&gt; transactedHolder = ThreadLocal.withInitial(() -&gt; false);</span><br><span class="line"></span><br><span class="line">    public static boolean inTransaction() &#123;</span><br><span class="line">        return transactedHolder.get();</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    public static Connection get() &#123;</span><br><span class="line">        </span><br><span class="line">        Connection connection = connectionHolder.get();</span><br><span class="line">        if (transactedHolder.get() &amp;&amp; connection != null) &#123;</span><br><span class="line">            try &#123;</span><br><span class="line">                connection.setAutoCommit(false);</span><br><span class="line">            &#125; catch (SQLException e) &#123;</span><br><span class="line">                e.printStackTrace();</span><br><span class="line">            &#125;</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        return connection;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    public static void set(Connection value) &#123;</span><br><span class="line">        if (transactedHolder.get()) &#123;</span><br><span class="line">            try &#123;</span><br><span class="line">                value.setAutoCommit(false);</span><br><span class="line">            &#125; catch (SQLException e) &#123;</span><br><span class="line">                e.printStackTrace();</span><br><span class="line">            &#125;</span><br><span class="line">        &#125;</span><br><span class="line">        connectionHolder.set(value);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    public static void close() &#123;</span><br><span class="line">        if (transactedHolder.get()) &#123;</span><br><span class="line">            return;</span><br><span class="line">        &#125;</span><br><span class="line">        try &#123;</span><br><span class="line">            Connection connection = connectionHolder.get();</span><br><span class="line">            connectionHolder.remove();</span><br><span class="line">            if (connection != null) &#123;</span><br><span class="line">                connection.close();</span><br><span class="line">            &#125;</span><br><span class="line">        &#125; catch (SQLException e) &#123;</span><br><span class="line">            e.printStackTrace();</span><br><span class="line">            throw new LSJdbcException(e);</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    public static void beginTransaction() &#123;</span><br><span class="line">        get();//设置AutoCommit为false</span><br><span class="line">        transactedHolder.set(true);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    public static void commitTransaction() throws SQLException&#123;</span><br><span class="line">        Connection conn = connectionHolder.get();</span><br><span class="line">        if (conn != null)&#123;</span><br><span class="line">            if (!conn.getAutoCommit())&#123;</span><br><span class="line">                conn.commit();</span><br><span class="line">            &#125;</span><br><span class="line">            transactedHolder.set(false);</span><br><span class="line">            close(); //提交事务后关闭连接</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    public static void rollbackTransaction()&#123;</span><br><span class="line">        Connection conn = connectionHolder.get();</span><br><span class="line">        try &#123;</span><br><span class="line">            if (conn != null)&#123;</span><br><span class="line">                if(!conn.getAutoCommit())&#123;</span><br><span class="line">                    conn.rollback();</span><br><span class="line">                &#125;</span><br><span class="line">                transactedHolder.set(false);</span><br><span class="line">                close();//回滚后关闭连接</span><br><span class="line">            &#125;</span><br><span class="line">        &#125;catch(SQLException e)&#123;</span><br><span class="line">            e.printStackTrace();</span><br><span class="line">            throw new LSJdbcException(e);</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>
<p>就是对两个ThreadLocal的各种基本操作而已，没有什么疑难的地方。</p>
<h4 id="JdbcExecutor"><a href="#JdbcExecutor" class="headerlink" title="JdbcExecutor"></a>JdbcExecutor</h4><p>最后就是真正执行SQL的执行者做为压轴了。这也是一个接口，只有一个默认实现类DefaultJdbcExecutor。</p>
<figure class="highlight plain"><table><tr><td class="code"><pre><span class="line"></span><br><span class="line">public class DefaultJdbcExecutor implements JdbcExecutor &#123;</span><br><span class="line"></span><br><span class="line">    private boolean mapUnderscoreToCamelCase = false;</span><br><span class="line"></span><br><span class="line">    public DefaultJdbcExecutor(boolean mapUnderscoreToCamelCase) &#123;</span><br><span class="line">        this.mapUnderscoreToCamelCase = mapUnderscoreToCamelCase;</span><br><span class="line">    &#125;</span><br><span class="line"> </span><br><span class="line">    @Override</span><br><span class="line">    public long executeUpdate(Connection connection, String sql, Object[] paras)&#123;</span><br><span class="line">        PreparedStatement preparedStatement = null;</span><br><span class="line">        ResultSet resultSet = null;</span><br><span class="line">        try &#123;</span><br><span class="line">            preparedStatement = connection.prepareStatement(sql);</span><br><span class="line"></span><br><span class="line">            fillPreparedStatementParas(preparedStatement, paras);</span><br><span class="line"></span><br><span class="line">            Integer code = preparedStatement.executeUpdate();</span><br><span class="line"></span><br><span class="line">            //尝试加上插入数据返回插入的主键，暂时没有好的思路</span><br><span class="line">//            if (sql.toLowerCase().contains(&quot;insert&quot;)) &#123;</span><br><span class="line">//                try &#123;</span><br><span class="line">//                    resultSet = preparedStatement.getGeneratedKeys();</span><br><span class="line">//                    PreparedStatement statement = connection.prepareStatement(&quot;SELECT LAST_INSERT_ID()&quot;);</span><br><span class="line">//                    if (resultSet.next()) &#123;</span><br><span class="line">//                        return resultSet.getLong(1);</span><br><span class="line">//                    &#125;</span><br><span class="line">//                &#125; catch (Exception e) &#123;</span><br><span class="line">//                    e.printStackTrace();</span><br><span class="line">//                    throw new LSJdbcException(e);</span><br><span class="line">//                &#125;</span><br><span class="line">//            &#125;</span><br><span class="line"></span><br><span class="line">            return code.longValue();</span><br><span class="line"></span><br><span class="line">        &#125; catch (SQLException e) &#123;</span><br><span class="line">            e.printStackTrace();</span><br><span class="line">            throw new LSJdbcException(e);</span><br><span class="line">        &#125; finally &#123;</span><br><span class="line">            close(resultSet, preparedStatement, connection);</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    @Override</span><br><span class="line">    public List&lt;Map&lt;String, Object&gt;&gt; executeQuery(Connection connection, String sql, Object[] paras)&#123;</span><br><span class="line">        PreparedStatement preparedStatement = null;</span><br><span class="line">        ResultSet resultSet = null;</span><br><span class="line">        try &#123;</span><br><span class="line">            preparedStatement = connection.prepareStatement(sql);</span><br><span class="line"></span><br><span class="line">            fillPreparedStatementParas(preparedStatement, paras);</span><br><span class="line"></span><br><span class="line">            resultSet = preparedStatement.executeQuery();//查询结果</span><br><span class="line"></span><br><span class="line">            return parseResultSet(resultSet);</span><br><span class="line"></span><br><span class="line">        &#125; catch (SQLException e) &#123;</span><br><span class="line">            e.printStackTrace();</span><br><span class="line">            throw new LSJdbcException(e);</span><br><span class="line">        &#125; finally &#123;</span><br><span class="line">            close(resultSet, preparedStatement, connection);</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    @Override</span><br><span class="line">    public &lt;T&gt; List&lt;T&gt; queryBeanList(Connection connection, Class&lt;T&gt; clazz, String sql, Object[] paras)&#123;</span><br><span class="line">        List&lt;Map&lt;String, Object&gt;&gt; resultList = executeQuery(connection, sql, paras);</span><br><span class="line">        return JdbcRSConvertUtil.convertObjectList(clazz, resultList);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    @Override</span><br><span class="line">    public &lt;T&gt; T queryBean(Connection connection, Class&lt;T&gt; clazz, String sql, Object[] paras)&#123;</span><br><span class="line">        List&lt;T&gt; list = queryBeanList(connection, clazz, sql, paras);</span><br><span class="line">        return CollectionKit.isEmptyCollection(list) ? null : list.get(0);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    @Override</span><br><span class="line">    public Number queryNumber(Connection connection, String sql, Object[] paras)&#123;</span><br><span class="line">        PreparedStatement preparedStatement = null;</span><br><span class="line">        ResultSet resultSet = null;</span><br><span class="line">        Number result = 0;</span><br><span class="line">        try &#123;</span><br><span class="line">            preparedStatement = connection.prepareStatement(sql);</span><br><span class="line"></span><br><span class="line">            fillPreparedStatementParas(preparedStatement, paras);</span><br><span class="line"></span><br><span class="line">            resultSet = preparedStatement.executeQuery();//查询结果</span><br><span class="line"></span><br><span class="line">            if(resultSet.next())&#123;</span><br><span class="line">                result = resultSet.getBigDecimal(1);</span><br><span class="line">            &#125;</span><br><span class="line"></span><br><span class="line">            return result;</span><br><span class="line"></span><br><span class="line">        &#125; catch (SQLException e) &#123;</span><br><span class="line">            e.printStackTrace();</span><br><span class="line">            throw new LSJdbcException(e);</span><br><span class="line">        &#125; finally &#123;</span><br><span class="line">            close(resultSet, preparedStatement, connection);</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">    </span><br><span class="line">    private void close(ResultSet resultSet, PreparedStatement preparedStatement, Connection connection)&#123;</span><br><span class="line"></span><br><span class="line">        try &#123;</span><br><span class="line">            if (resultSet != null) &#123;</span><br><span class="line">                resultSet.close();</span><br><span class="line">            &#125;</span><br><span class="line">            if (preparedStatement != null) &#123;</span><br><span class="line">                preparedStatement.close();</span><br><span class="line">            &#125;</span><br><span class="line">        &#125; catch (SQLException e) &#123;</span><br><span class="line">            e.printStackTrace();</span><br><span class="line">        &#125;</span><br><span class="line">        ConnectionContext.close();</span><br><span class="line">    &#125;</span><br><span class="line">    </span><br><span class="line">    private void fillPreparedStatementParas(PreparedStatement preparedStatement, Object[] paras) throws SQLException &#123;</span><br><span class="line">        if (null == paras)</span><br><span class="line">            return;</span><br><span class="line">        int pos = 1;</span><br><span class="line">        for (Object obj : paras)&#123;</span><br><span class="line">            if (obj.getClass().isArray()) &#123; //填充数组数据</span><br><span class="line">                int len = Array.getLength(obj);</span><br><span class="line">                for (int j = 0; j &lt; len; j++) &#123;</span><br><span class="line">                    preparedStatement.setObject(pos++, Array.get(obj, j));</span><br><span class="line">                &#125;</span><br><span class="line">                continue;</span><br><span class="line">            &#125;</span><br><span class="line">            preparedStatement.setObject(pos++, obj);</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    private List&lt;Map&lt;String, Object&gt;&gt; parseResultSet(ResultSet resultSet) &#123;</span><br><span class="line">        List&lt;Map&lt;String, Object&gt;&gt; resultList = new ArrayList&lt;&gt;();</span><br><span class="line">        try &#123;</span><br><span class="line">            ResultSetMetaData metaData = resultSet.getMetaData();</span><br><span class="line">            while (resultSet.next()) &#123;</span><br><span class="line">                Map&lt;String, Object&gt; map = new HashMap&lt;&gt;();</span><br><span class="line">                for (int i = 1; i &lt;= metaData.getColumnCount(); i++) &#123;</span><br><span class="line">                    String name = metaData.getColumnName(i);</span><br><span class="line">                    if (mapUnderscoreToCamelCase) &#123;</span><br><span class="line">                        name = StringKit.toCamelCase(name);</span><br><span class="line">                    &#125;</span><br><span class="line">                    Object val = resultSet.getObject(i);</span><br><span class="line">                    map.put(name, val);</span><br><span class="line">                &#125;</span><br><span class="line">                resultList.add(map);</span><br><span class="line">            &#125;</span><br><span class="line">        &#125; catch (SQLException e) &#123;</span><br><span class="line">            e.printStackTrace();</span><br><span class="line">            throw new LSJdbcException(e);</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        return resultList;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>
<p>基本的JDBC应用封装，全都用PreparedStatement，安全还有据说性能稍好。</p>
<p>其中parseResultSet把ResultSet转成Map，应该丢进工具类。还有结果解析这部分的代码是准备重构，大概方向就是不硬编码写成这样，模仿JdbcTemplate留一个RowMapper接口，提供一个默认的转换成JavaBean的实现类和一个转成Map的实现类，再在MapperData里存具体的RowMapper，提供注解覆盖默认实现（优先级：方法级注解 -&gt; 类级注解 -&gt; 默认实现），目前的方案虽然是实现了功能，但进行两次转换（ResultSet -&gt; List&lt;Map&lt;String, Object&gt;&gt;, List&lt;Map&lt;String, Object&gt;&gt; -&gt; List<object>）还是会对性能有影响。</object></p>
<h4 id="附加"><a href="#附加" class="headerlink" title="附加"></a>附加</h4><p>下面是跟core模块配合使用时的附加功能，不需要了解的可以直接跳过……</p>
<p>先是Loader，框架启动时获取Mapper强化类放到Bean容器，重新进行依赖注入。然后是给类或类里的某个方法有LSTransacted注解的类加上事务拦截。</p>
<figure class="highlight plain"><table><tr><td class="code"><pre><span class="line"></span><br><span class="line">@LSLoader(20)</span><br><span class="line">public class JdbcLoader implements Loader &#123;</span><br><span class="line">    @Override</span><br><span class="line">    public void doLoad(Set&lt;Class&lt;?&gt;&gt; classSet) &#123;</span><br><span class="line"></span><br><span class="line">        SqlSession sqlSession = BeanContainer.getBean(&quot;sqlSession&quot;); //从Bean容器获取sqlsession</span><br><span class="line">        if (sqlSession == null) &#123;</span><br><span class="line">            throw new LSJdbcException(&quot;sqlSession is null&quot;);</span><br><span class="line">        &#125;</span><br><span class="line">        //将所有LSMapper注解的类加入Bean容器</span><br><span class="line">        Set&lt;Class&lt;?&gt;&gt; mapperClassList = ClassUtil.getClassesByAnnotation(classSet, LSMapper.class);</span><br><span class="line">        for (Class&lt;?&gt; clazz : mapperClassList) &#123;</span><br><span class="line">            Object mapper = sqlSession.getMapper(clazz);</span><br><span class="line">            BeanContainer.put(clazz.getName(), mapper);</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        DependencyInjector.injectAll(); //重新进行依赖注入</span><br><span class="line"></span><br><span class="line">        //给所有LSBean注解并且类里包含LSTransacted注解的类加上事务拦截</span><br><span class="line">        TransactionAopAction transactionAopAction = new TransactionAopAction();</span><br><span class="line">        BeanContainer.put(TransactionAopAction.class.getName(), transactionAopAction);</span><br><span class="line">        ClassUtil.getClassesByAnnotation(classSet, LSBean.class)</span><br><span class="line">                .stream()</span><br><span class="line">                .filter(clazz -&gt; ClassUtil.hasAnnotation(clazz, LSTransacted.class))</span><br><span class="line">                .forEach(clazz -&gt; &#123;</span><br><span class="line">                    List&lt;AopAction&gt; list = AopContainer.getClassAopActionChainOrNew(clazz);</span><br><span class="line">                    list.add(transactionAopAction);</span><br><span class="line">                    AopContainer.putClassAopActionChain(clazz, list);</span><br><span class="line">                &#125;);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line"></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>
<p>@Loader(20)的意思是把让这个JdbcLoader在IocLoader之后、AopLoader执行之前执行，不然因为Aop强化的判断逻辑有可能会导致没强化这些有事务注解的类而导致事务不生效（具体可看AopHelper的代码）。</p>
<p>然后是事务拦截器</p>
<figure class="highlight plain"><table><tr><td class="code"><pre><span class="line"></span><br><span class="line">public class TransactionAopAction extends AopAction &#123;</span><br><span class="line">    @Override</span><br><span class="line">    public void invoke(Invocation invocation) throws Throwable &#123;</span><br><span class="line"></span><br><span class="line">        if (!invocation.getMethod().isAnnotationPresent(LSTransacted.class)) &#123;</span><br><span class="line">            invocation.invoke();//没有事务注解直接执行</span><br><span class="line">            return;</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        if (ConnectionContext.inTransaction()) &#123;</span><br><span class="line">            invocation.invoke();</span><br><span class="line">            return;</span><br><span class="line">        &#125;</span><br><span class="line">        try &#123;</span><br><span class="line">            ConnectionContext.beginTransaction();</span><br><span class="line">            invocation.invoke();</span><br><span class="line">            ConnectionContext.commitTransaction();</span><br><span class="line">        &#125; catch (Exception e) &#123;</span><br><span class="line">            ConnectionContext.rollbackTransaction();</span><br><span class="line">            throw e;//异常继续往上抛</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>
<p>都是调用ConnectionContext封装的方法。具体逻辑不用多说显而易见，其中有个判断当前连接是否处于事务之中，是为了简单的解决事务的传播（一个有事务注解的业务方法调用另一个有事务注解的业务方法）。</p>
<h4 id="结语"><a href="#结语" class="headerlink" title="结语"></a>结语</h4><p>到这大概就算是实现了一个简易版的“Mybatis”了，其实写笔记的时候想起好几处可以优化的地方，以及发现了好几个简单的单元测试没测出来的bug。使用方法可以直接在IOC那篇笔记找到git地址看test的代码。</p>
<p>其实健壮性还是差挺多，不过也还是起到了一定的学习作用吧，其实一开始是有个想法模仿spring-data-jpa的，但是想了想解析方法名生成SQL语句需要的时间对我来说应该还是有点多的，最后就放弃了，等有时间的时候再尝试一下，当然有时间也得先把现有的问题给改了~</p>
<p>最后，虽然关注我专栏的朋友只有寥寥几个，但是如果我的笔记有帮到你们的话还是希望你们可以帮我向朋友们推广一下或者给项目点给star，或者有工作机会的也可以给我介绍一下，在此感谢。</p>
<p>准备秋招中，秋招结束之前估计是不准备再写笔记了~</p>

                <hr>
                

                <ul class="pager">
                    
                    
                    <li class="next">
                        <a href="/2018/07/18/JVM类加载简单笔记/" data-toggle="tooltip" data-placement="top"
                           title="JVM类加载简单笔记">Next Post &rarr;</a>
                    </li>
                    
                </ul>

                

                


                <!--加入新的评论系统-->
                
            </div>

            <div class="hidden-xs col-sm-3 toc-col">
                <div class="toc-wrap">
                    <ol class="toc"><li class="toc-item toc-level-4"><a class="toc-link" href="#前言"><span class="toc-text">前言</span></a></li><li class="toc-item toc-level-4"><a class="toc-link" href="#实现功能"><span class="toc-text">实现功能</span></a></li><li class="toc-item toc-level-4"><a class="toc-link" href="#注解类"><span class="toc-text">注解类</span></a></li><li class="toc-item toc-level-4"><a class="toc-link" href="#执行的方法和SQL直接的映射关系"><span class="toc-text">执行的方法和SQL直接的映射关系</span></a></li><li class="toc-item toc-level-4"><a class="toc-link" href="#代理类"><span class="toc-text">代理类</span></a></li><li class="toc-item toc-level-4"><a class="toc-link" href="#实现SqlSession"><span class="toc-text">实现SqlSession</span></a></li><li class="toc-item toc-level-4"><a class="toc-link" href="#ConnectionContext"><span class="toc-text">ConnectionContext</span></a></li><li class="toc-item toc-level-4"><a class="toc-link" href="#JdbcExecutor"><span class="toc-text">JdbcExecutor</span></a></li><li class="toc-item toc-level-4"><a class="toc-link" href="#附加"><span class="toc-text">附加</span></a></li><li class="toc-item toc-level-4"><a class="toc-link" href="#结语"><span class="toc-text">结语</span></a></li></ol>
                </div>
            </div>
        </div>

        <div class="row">
            <!-- Sidebar Container -->

            <div class="
                col-lg-8 col-lg-offset-2
                col-md-10 col-md-offset-1
                sidebar-container">

                <!-- Featured Tags -->
                
                <section>
                    <!-- no hr -->
                    <h5 class="text-center">
                        <a href="/tags/">FEATURED TAGS</a>
                    </h5>
                    <div class="tags">
                        
                        <a class="tag" href="/tags/#java"
                           title="java">java</a>
                        
                        <a class="tag" href="/tags/#spring"
                           title="spring">spring</a>
                        
                    </div>
                </section>
                

                <!-- Friends Blog -->
                
            </div>
        </div>

    </div>
</article>







<!-- Footer -->
<!-- Footer -->
<footer>
    <div class="container">
        <div class="row">
            <div class="col-lg-8 col-lg-offset-2 col-md-10 col-md-offset-1 text-center">
                <br>
                <ul class="list-inline text-center">
                
                
                
                    <li>
                        <a target="_blank" href="https://www.zhihu.com/people/asdf1234-97">
                            <span class="fa-stack fa-lg">
                                <i class="fa fa-circle fa-stack-2x"></i>
                                <i class="fa  fa-stack-1x fa-inverse">知</i>
                            </span>
                        </a>
                    </li>
                

                

                

                
                    <li>
                        <a target="_blank"  href="https://github.com/xiuqianli1996">
                            <span class="fa-stack fa-lg">
                                <i class="fa fa-circle fa-stack-2x"></i>
                                <i class="fa fa-github fa-stack-1x fa-inverse"></i>
                            </span>
                        </a>
                    </li>
                

                

                </ul>
                <p class="copyright text-muted">
                    Copyright &copy; Xiuqianli&#39;s blog 2018
                    <br>
                    <span id="busuanzi_container_site_pv" style="font-size: 12px;">PV: <span id="busuanzi_value_site_pv"></span> Times</span>
                    <br>
                    Theme by <a href="https://haojen.github.io/">Haojen Ma</a>
                </p>

            </div>
        </div>
    </div>
</footer>

<!-- jQuery -->
<script src="/js/jquery.min.js"></script>

<!-- Bootstrap Core JavaScript -->
<script src="/js/bootstrap.min.js"></script>

<!-- Custom Theme JavaScript -->
<script src="/js/blog.js"></script>

<!-- async load function -->
<script>
    function async(u, c) {
      var d = document, t = 'script',
          o = d.createElement(t),
          s = d.getElementsByTagName(t)[0];
      o.src = u;
      if (c) { o.addEventListener('load', function (e) { c(null, e); }, false); }
      s.parentNode.insertBefore(o, s);
    }
</script>

<!-- jquery.tagcloud.js -->
<script>
    // only load tagcloud.js in tag.html
    if($('#tag_cloud').length !== 0){
        async("https://981764793.gitee.io/js/jquery.tagcloud.js",function(){
            $.fn.tagcloud.defaults = {
                //size: {start: 1, end: 1, unit: 'em'},
                color: {start: '#bbbbee', end: '#0085a1'},
            };
            $('#tag_cloud a').tagcloud();
        })
    }
</script>

<!--fastClick.js -->
<script>
    async("//cdn.bootcss.com/fastclick/1.0.6/fastclick.min.js", function(){
        var $nav = document.querySelector("nav");
        if($nav) FastClick.attach($nav);
    })
</script>

<!-- Google Analytics -->



<!-- Baidu Tongji -->


<!-- swiftype -->
<script type="text/javascript">
  (function(w,d,t,u,n,s,e){w['SwiftypeObject']=n;w[n]=w[n]||function(){
  (w[n].q=w[n].q||[]).push(arguments);};s=d.createElement(t);
  e=d.getElementsByTagName(t)[0];s.async=1;s.src=u;e.parentNode.insertBefore(s,e);
  })(window,document,'script','//s.swiftypecdn.com/install/v2/st.js','_st');

  _st('install','null','2.0.0');
</script>

<script async src="//dn-lbstatics.qbox.me/busuanzi/2.3/busuanzi.pure.mini.js"></script>

<!--wechat title img-->
<img class="wechat-title-img" src="null">
</body>

</html>
